/*
 * Copyright (C)  Tony Green, LitePal Framework Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.litepal.lib;


import com.litepal.lib.annotation.Column;
import com.litepal.lib.crud.DataSupport;
import com.litepal.lib.crud.model.AssociationsInfo;
import com.litepal.lib.exceptions.DatabaseGenerateException;
import com.litepal.lib.parser.LitePalAttr;
import com.litepal.lib.tablemanager.model.AssociationsModel;
import com.litepal.lib.tablemanager.model.ColumnModel;
import com.litepal.lib.tablemanager.model.TableModel;
import com.litepal.lib.tablemanager.typechange.BlobOrm;
import com.litepal.lib.tablemanager.typechange.BooleanOrm;
import com.litepal.lib.tablemanager.typechange.DateOrm;
import com.litepal.lib.tablemanager.typechange.DecimalOrm;
import com.litepal.lib.tablemanager.typechange.NumericOrm;
import com.litepal.lib.tablemanager.typechange.OrmChange;
import com.litepal.lib.tablemanager.typechange.TextOrm;
import com.litepal.lib.util.BaseUtility;
import com.litepal.lib.util.Const;
import com.litepal.lib.util.DBUtility;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Base class of all the LitePal components. If each component need to
 * interactive with other components or they have some same logic with duplicate
 * codes, LitePalBase may be the solution.
 *
 * @author Tony Green
 * @since 1.1
 */
public abstract class LitePalBase
{

	public static final String TAG = "LitePalBase";

	/**
	 * Action to get associations.
	 */
	private static final int GET_ASSOCIATIONS_ACTION = 1;

	/**
	 * Action to get association info.
	 */
	private static final int GET_ASSOCIATION_INFO_ACTION = 2;

	/**
	 * All the supporting mapping types currently in the array.
	 */
	private OrmChange[] typeChangeRules = {new NumericOrm(), new TextOrm(), new BooleanOrm(), new DecimalOrm(), new DateOrm(), new BlobOrm()};

	/**
	 * This is map of class name to fields list. Indicates that each class has which fields.
	 */
	private Map<String, List<Field>> classFieldsMap = new HashMap<String, List<Field>>();

	/**
	 * The collection contains all association models.
	 */
	private Collection<AssociationsModel> mAssociationModels;

	/**
	 * The collection contains all association info.
	 */
	private Collection<AssociationsInfo> mAssociationInfos;

	/**
	 * This method is used to get the table model by the class name passed
	 * in. The principle to generate table model is that each field in the class
	 * with non-static modifier and has a type among int/Integer, long/Long,
	 * short/Short, float/Float, double/Double, char/Character, boolean/Boolean
	 * or String, would generate a column with same name as corresponding field.
	 * If users don't want some of the fields map a column, declare an ignore
	 * annotation with {@link Column#ignore()}.
	 *
	 * @param className The full name of the class to map in database.
	 * @return A table model with table name, class name and the map of column
	 * name and column type.
	 */
	protected TableModel getTableModel(String className)
	{
		String tableName = DBUtility.getTableNameByClassName(className);
		TableModel tableModel = new TableModel();
		tableModel.setTableName(tableName);
		tableModel.setClassName(className);
		List<Field> supportedFields = getSupportedFields(className);
		for (Field field : supportedFields)
		{
			ColumnModel columnModel = convertFieldToColumnModel(field);
			tableModel.addColumnModel(columnModel);
		}
		return tableModel;
	}

	/**
	 * This method is used to get association models depends on the given class
	 * name list.
	 *
	 * @param classNames The names of the classes that want to get their associations.
	 * @return Collection of association models.
	 */
	protected Collection<AssociationsModel> getAssociations(List<String> classNames)
	{
		if (mAssociationModels == null)
		{
			mAssociationModels = new HashSet<AssociationsModel>();
		}
		mAssociationModels.clear();
		for (String className : classNames)
		{
			analyzeClassFields(className, GET_ASSOCIATIONS_ACTION);
		}
		return mAssociationModels;
	}

	/**
	 * Get the association info model by the class name.
	 *
	 * @param className The class name to introspection.
	 * @return Collection of association info.
	 */
	protected Collection<AssociationsInfo> getAssociationInfo(String className)
	{
		if (mAssociationInfos == null)
		{
			mAssociationInfos = new HashSet<AssociationsInfo>();
		}
		mAssociationInfos.clear();
		analyzeClassFields(className, GET_ASSOCIATION_INFO_ACTION);
		return mAssociationInfos;
	}

	/**
	 * Find all the fields in the class. But not each field is supported to add
	 * a column to the table. Only the basic data types and String are
	 * supported. This method will intercept all the types which are not
	 * supported and return a new list of supported fields.
	 *
	 * @param className The full name of the class.
	 * @return A list of supported fields
	 */
	protected List<Field> getSupportedFields(String className)
	{
		List<Field> fieldList = classFieldsMap.get(className);
		if (fieldList == null)
		{
			List<Field> supportedFields = new ArrayList<Field>();
			Class<?> clazz;
			try
			{
				clazz = Class.forName(className);
			}
			catch (ClassNotFoundException e)
			{
				throw new DatabaseGenerateException(DatabaseGenerateException.CLASS_NOT_FOUND + className);
			}
			recursiveSupportedFields(clazz, supportedFields);
			classFieldsMap.put(className, supportedFields);
			return supportedFields;
		}
		return fieldList;
	}

	/**
	 * If the field type implements from List or Set, regard it as a collection.
	 *
	 * @param fieldType The field type.
	 * @return True if the field type is collection, false otherwise.
	 */
	protected boolean isCollection(Class<?> fieldType)
	{
		return isList(fieldType) || isSet(fieldType);
	}

	/**
	 * If the field type implements from List, regard it as a list.
	 *
	 * @param fieldType The field type.
	 * @return True if the field type is List, false otherwise.
	 */
	protected boolean isList(Class<?> fieldType)
	{
		return List.class.isAssignableFrom(fieldType);
	}

	/**
	 * If the field type implements from Set, regard it as a set.
	 *
	 * @param fieldType The field type.
	 * @return True if the field type is Set, false otherwise.
	 */
	protected boolean isSet(Class<?> fieldType)
	{
		return Set.class.isAssignableFrom(fieldType);
	}

	/**
	 * Judge the passed in column is an id column or not. The column named id or
	 * _id will be considered as id column.
	 *
	 * @param columnName The name of column.
	 * @return Return true if it's id column, otherwise return false.
	 */
	protected boolean isIdColumn(String columnName)
	{
		return "_id".equalsIgnoreCase(columnName) || "id".equalsIgnoreCase(columnName);
	}

	/**
	 * If two tables are associated, one table have a foreign key column. The
	 * foreign key column name will be the associated table name with _id
	 * appended.
	 *
	 * @param associatedTableName The associated table name.
	 * @return The foreign key column name.
	 */
	protected String getForeignKeyColumnName(String associatedTableName)
	{
		return BaseUtility.changeCase(associatedTableName + "_id");
	}

	private void recursiveSupportedFields(Class<?> clazz, List<Field> supportedFields)
	{
		if (clazz == DataSupport.class || clazz == Object.class)
		{
			return;
		}
		Field[] fields = clazz.getDeclaredFields();
		if (fields != null && fields.length > 0)
		{
			for (Field field : fields)
			{
				Column annotation = field.getAnnotation(Column.class);
				if (annotation != null && annotation.ignore())
				{
					continue;
				}
				int modifiers = field.getModifiers();
				if (!Modifier.isStatic(modifiers))
				{
					Class<?> fieldTypeClass = field.getType();
					String fieldType = fieldTypeClass.getName();
					if (BaseUtility.isFieldTypeSupported(fieldType))
					{
						supportedFields.add(field);
					}
				}
			}
		}
		recursiveSupportedFields(clazz.getSuperclass(), supportedFields);
	}

	/**
	 * Introspection of the passed in class. Analyze the fields of current class
	 * and find out the associations of it.
	 *
	 * @param className The class name to introspection.
	 * @param action    Between {@link com.litepal.lib.LitePalBase#GET_ASSOCIATIONS_ACTION} and
	 *                  {@link com.litepal.lib.LitePalBase#GET_ASSOCIATION_INFO_ACTION}
	 */
	private void analyzeClassFields(String className, int action)
	{
		try
		{
			Class<?> dynamicClass = Class.forName(className);
			Field[] fields = dynamicClass.getDeclaredFields();
			for (Field field : fields)
			{
				if (isPrivateAndNonPrimitive(field))
				{
					oneToAnyConditions(className, field, action);
					manyToAnyConditions(className, field, action);
				}
			}
		}
		catch (ClassNotFoundException ex)
		{
			ex.printStackTrace();
			throw new DatabaseGenerateException(DatabaseGenerateException.CLASS_NOT_FOUND + className);
		}
	}

	/**
	 * Judge the field is a private non primitive field or not.
	 *
	 * @param field The field to judge.
	 * @return True if the field is <b>private</b> and <b>non primitive</b>,
	 * false otherwise.
	 */
	private boolean isPrivateAndNonPrimitive(Field field)
	{
		return Modifier.isPrivate(field.getModifiers()) && !field.getType().isPrimitive();
	}

	/**
	 * Deals with one to any association conditions. e.g. Song and Album. An
	 * album have many songs, and a song belongs to one album. So if there's an
	 * Album model defined in Song with private modifier, and in Album there's a
	 * List or Set with generic type of Song and declared as private modifier,
	 * they are one2many association. If there's no List or Set defined in
	 * Album, they will become one2one associations. If there's also a Song
	 * model defined in Album with private modifier, maybe the album just have
	 * one song, they are one2one association too.
	 * <p/>
	 * When it's many2one association, it's easy to just simply add a foreign id
	 * column to the many side model's table. But when it comes to many2many
	 * association, it can not be done without intermediate join table in
	 * database. LitePal assumes that this join table's name is the
	 * concatenation of the two target table names in alphabetical order.
	 *
	 * @param className Source class name.
	 * @param field     A field of source class.
	 * @param action    Between {@link com.litepal.lib.LitePalBase#GET_ASSOCIATIONS_ACTION} and
	 *                  {@link com.litepal.lib.LitePalBase#GET_ASSOCIATION_INFO_ACTION}
	 * @throws ClassNotFoundException
	 */
	private void oneToAnyConditions(String className, Field field, int action) throws ClassNotFoundException
	{
		Class<?> fieldTypeClass = field.getType();
		// If the mapping list contains the class name
		// defined in one class.
		if (LitePalAttr.getInstance().getClassNames().contains(fieldTypeClass.getName()))
		{
			Class<?> reverseDynamicClass = Class.forName(fieldTypeClass.getName());
			Field[] reverseFields = reverseDynamicClass.getDeclaredFields();
			// Look up if there's a reverse association
			// definition in the reverse class.
			boolean reverseAssociations = false;
			// Begin to check the fields of the defined
			// class.
			for (int i = 0; i < reverseFields.length; i++)
			{
				Field reverseField = reverseFields[i];
				if (!Modifier.isStatic(reverseField.getModifiers()))
				{
					Class<?> reverseFieldTypeClass = reverseField.getType();
					// If there's the from class name in the
					// defined class, they are one2one bidirectional
					// associations.
					if (className.equals(reverseFieldTypeClass.getName()))
					{
						if (action == GET_ASSOCIATIONS_ACTION)
						{
							addIntoAssociationModelCollection(className, fieldTypeClass.getName(), fieldTypeClass.getName(), Const.Model.ONE_TO_ONE);
						}
						else if (action == GET_ASSOCIATION_INFO_ACTION)
						{
							addIntoAssociationInfoCollection(className, fieldTypeClass.getName(), fieldTypeClass.getName(), field, reverseField, Const.Model.ONE_TO_ONE);
						}
						reverseAssociations = true;
					}
					// If there's the from class Set or List in
					// the defined class, they are many2one bidirectional
					// associations.
					else if (isCollection(reverseFieldTypeClass))
					{
						String genericTypeName = getGenericTypeName(reverseField);
						if (className.equals(genericTypeName))
						{
							if (action == GET_ASSOCIATIONS_ACTION)
							{
								addIntoAssociationModelCollection(className, fieldTypeClass.getName(), className, Const.Model.MANY_TO_ONE);
							}
							else if (action == GET_ASSOCIATION_INFO_ACTION)
							{
								addIntoAssociationInfoCollection(className, fieldTypeClass.getName(), className, field, reverseField, Const.Model.MANY_TO_ONE);
							}
							reverseAssociations = true;
						}
					}
				}
			}
			// If there's no from class in the defined class, they are
			// one2one unidirectional associations.
			if (!reverseAssociations)
			{
				if (action == GET_ASSOCIATIONS_ACTION)
				{
					addIntoAssociationModelCollection(className, fieldTypeClass.getName(), fieldTypeClass.getName(), Const.Model.ONE_TO_ONE);
				}
				else if (action == GET_ASSOCIATION_INFO_ACTION)
				{
					addIntoAssociationInfoCollection(className, fieldTypeClass.getName(), fieldTypeClass.getName(), field, null, Const.Model.ONE_TO_ONE);
				}
			}
		}
	}

	/**
	 * Deals with one to any association conditions. e.g. Song and Album. An
	 * album have many songs, and a song belongs to one album. So if there's an
	 * Album model defined in Song with private modifier, and in Album there's a
	 * List or Set with generic type of Song and declared as private modifier,
	 * they are one2many association. If there's no List or Set defined in
	 * Album, they will become one2one associations. If there's also a Song
	 * model defined in Album with private modifier, maybe the album just have
	 * one song, they are one2one association too.
	 * <p/>
	 * When it's many2one association, it's easy to just simply add a foreign id
	 * column to the many side model's table. But when it comes to many2many
	 * association, it can not be done without intermediate join table in
	 * database. LitePal assumes that this join table's name is the
	 * concatenation of the two target table names in alphabetical order.
	 *
	 * @param className Source class name.
	 * @param field     A field of source class.
	 * @param action    Between {@link com.litepal.lib.LitePalBase#GET_ASSOCIATIONS_ACTION} and
	 *                  {@link com.litepal.lib.LitePalBase#GET_ASSOCIATION_INFO_ACTION}
	 * @throws ClassNotFoundException
	 */
	private void manyToAnyConditions(String className, Field field, int action) throws ClassNotFoundException
	{
		if (isCollection(field.getType()))
		{
			String genericTypeName = getGenericTypeName(field);
			// If the mapping list contains the genericTypeName, begin to check
			// this genericTypeName class.
			if (LitePalAttr.getInstance().getClassNames().contains(genericTypeName))
			{
				Class<?> reverseDynamicClass = Class.forName(genericTypeName);
				Field[] reverseFields = reverseDynamicClass.getDeclaredFields();
				// Look up if there's a reverse association
				// definition in the reverse class.
				boolean reverseAssociations = false;
				for (int i = 0; i < reverseFields.length; i++)
				{
					Field reverseField = reverseFields[i];
					// Only map private fields
					if (!Modifier.isStatic(reverseField.getModifiers()))
					{
						Class<?> reverseFieldTypeClass = reverseField.getType();
						// If there's a from class name defined in the reverse
						// class, they are many2one bidirectional
						// associations.
						if (className.equals(reverseFieldTypeClass.getName()))
						{
							if (action == GET_ASSOCIATIONS_ACTION)
							{
								addIntoAssociationModelCollection(className, genericTypeName, genericTypeName, Const.Model.MANY_TO_ONE);
							}
							else if (action == GET_ASSOCIATION_INFO_ACTION)
							{
								addIntoAssociationInfoCollection(className, genericTypeName, genericTypeName, field, reverseField, Const.Model.MANY_TO_ONE);
							}
							reverseAssociations = true;
						}
						// If there's a List or Set contains from class name
						// defined in the reverse class, they are many2many
						// association.
						else if (isCollection(reverseFieldTypeClass))
						{
							String reverseGenericTypeName = getGenericTypeName(reverseField);
							if (className.equals(reverseGenericTypeName))
							{
								if (action == GET_ASSOCIATIONS_ACTION)
								{
									addIntoAssociationModelCollection(className, genericTypeName, null, Const.Model.MANY_TO_MANY);
								}
								else if (action == GET_ASSOCIATION_INFO_ACTION)
								{
									addIntoAssociationInfoCollection(className, genericTypeName, null, field, reverseField, Const.Model.MANY_TO_MANY);
								}
								reverseAssociations = true;
							}
						}

					}
				}
				// If there's no from class in the defined class, they
				// are many2one unidirectional associations.
				if (!reverseAssociations)
				{
					if (action == GET_ASSOCIATIONS_ACTION)
					{
						addIntoAssociationModelCollection(className, genericTypeName, genericTypeName, Const.Model.MANY_TO_ONE);
					}
					else if (action == GET_ASSOCIATION_INFO_ACTION)
					{
						addIntoAssociationInfoCollection(className, genericTypeName, genericTypeName, field, null, Const.Model.MANY_TO_ONE);
					}
				}
			}
		}
	}

	/**
	 * Package a {@link com.litepal.lib.tablemanager.model.AssociationsModel}, and add it into
	 * {@link #mAssociationModels} Collection.
	 *
	 * @param className            The class name for {@link com.litepal.lib.tablemanager.model.AssociationsModel}.
	 * @param associatedClassName  The associated class name for {@link com.litepal.lib.tablemanager.model.AssociationsModel}.
	 * @param classHoldsForeignKey The class which holds foreign key.
	 * @param associationType      The association type for {@link com.litepal.lib.tablemanager.model.AssociationsModel}.
	 */
	private void addIntoAssociationModelCollection(String className, String associatedClassName, String classHoldsForeignKey, int associationType)
	{
		AssociationsModel associationModel = new AssociationsModel();
		associationModel.setTableName(DBUtility.getTableNameByClassName(className));
		associationModel.setAssociatedTableName(DBUtility.getTableNameByClassName(associatedClassName));
		associationModel.setTableHoldsForeignKey(DBUtility.getTableNameByClassName(classHoldsForeignKey));
		associationModel.setAssociationType(associationType);
		mAssociationModels.add(associationModel);
	}

	/**
	 * Package a {@link com.litepal.lib.crud.model.AssociationsInfo}, and add it into
	 * {@link #mAssociationInfos} Collection.
	 *
	 * @param selfClassName               The class name of self model.
	 * @param associatedClassName         The class name of the class which associated with self class.
	 * @param classHoldsForeignKey        The class which holds foreign key.
	 * @param associateOtherModelFromSelf The field of self class to declare has association with other
	 *                                    class.
	 * @param associateSelfFromOtherModel The field of the associated class to declare has association
	 *                                    with self class.
	 * @param associationType             The association type.
	 */
	private void addIntoAssociationInfoCollection(String selfClassName, String associatedClassName, String classHoldsForeignKey, Field associateOtherModelFromSelf, Field associateSelfFromOtherModel, int associationType)
	{
		AssociationsInfo associationInfo = new AssociationsInfo();
		associationInfo.setSelfClassName(selfClassName);
		associationInfo.setAssociatedClassName(associatedClassName);
		associationInfo.setClassHoldsForeignKey(classHoldsForeignKey);
		associationInfo.setAssociateOtherModelFromSelf(associateOtherModelFromSelf);
		associationInfo.setAssociateSelfFromOtherModel(associateSelfFromOtherModel);
		associationInfo.setAssociationType(associationType);
		mAssociationInfos.add(associationInfo);
	}

	/**
	 * Get the generic type name of List or Set. If there's no generic type of
	 * List or Set return null.
	 *
	 * @param field A generic type field.
	 * @return The name of generic type of List of Set.
	 */
	private String getGenericTypeName(Field field)
	{
		Type genericType = field.getGenericType();
		if (genericType != null)
		{
			if (genericType instanceof ParameterizedType)
			{
				ParameterizedType parameterizedType = (ParameterizedType) genericType;
				Class<?> genericArg = (Class<?>) parameterizedType.getActualTypeArguments()[0];
				return genericArg.getName();
			}
		}
		return null;
	}

	/**
	 * Convert a field instance into A ColumnModel instance. ColumnModel can provide information
	 * when creating table.
	 *
	 * @param field A supported field to map into column.
	 * @return ColumnModel instance contains column information.
	 */
	private ColumnModel convertFieldToColumnModel(Field field)
	{
		String columnType = null;
		String fieldType = field.getType().getName();
		for (OrmChange ormChange : typeChangeRules)
		{
			columnType = ormChange.object2Relation(fieldType);
			if (columnType != null)
			{
				break;
			}
		}
		boolean nullable = true;
		boolean unique = false;
		String defaultValue = "";
		Column annotation = field.getAnnotation(Column.class);
		if (annotation != null)
		{
			nullable = annotation.nullable();
			unique = annotation.unique();
			defaultValue = annotation.defaultValue();
		}
		ColumnModel columnModel = new ColumnModel();
		columnModel.setColumnName(field.getName());
		columnModel.setColumnType(columnType);
		columnModel.setIsNullable(nullable);
		columnModel.setIsUnique(unique);
		columnModel.setDefaultValue(defaultValue);
		return columnModel;
	}

}
